#include <iostream>
#include <fstream>
#include <cstring>
#include <map>
#include <list>
#ifdef WIN32
  #include <windows.h>
  #include <direct.h>
#else
  #include <sys/stat.h>
#endif

#include "tinyxml.h"

#include "Misc.h"
#include "Rom.h"

#include "modules/GameModule.h"
#include "modules/GenericGameModule.h"
#include "modules/EbModule.h"

#define VERSION "0.01"

using namespace std;

void usage(char* basename) {
  cout << "ebhack " << VERSION << endl;
  cout << "Usage:" << endl;
  cout << "       " << basename << " help" << endl;
//  cout << "       " << basename << " about" << endl;
  cout << "       " << basename << " i[mport] -i <rom> -o <directory>" << endl;
  cout << "       " << basename << " e[xport] -i <directory> -r <clean rom> -c <cccompiler exe> -o <out rom>" << endl;
}

void readProjectFile(char* dir, map<string, string> &files, list<string*> &scriptFiles, char* romtype) {
  char buf[100];
  strcpy(buf, dir);
  strcat(buf, "/project.ebproj");

  TiXmlDocument doc(buf);
  if (!doc.LoadFile()) {
    cerr << "Error: Failed to read project file at " << buf << endl;
    return;
  }
  TiXmlHandle hDoc(&doc);
  TiXmlElement* elem;
  TiXmlHandle hRoot(0), hEntry(0);

  // Get root element
  elem = hDoc.FirstChildElement().Element();
  hRoot = TiXmlHandle(elem);

  strncpy(romtype, hRoot.FirstChild("RomType").Element()->GetText(), 11);

  TiXmlElement* entryNode = hRoot.FirstChild("Files").Element();
  hEntry = TiXmlHandle(entryNode); 
  entryNode = hEntry.FirstChild("File").Element();
  for (; entryNode; entryNode = entryNode->NextSiblingElement()) {
    files.insert(pair<string, string>(entryNode->Attribute("id"), entryNode->GetText()));
  }

  entryNode = hRoot.FirstChild("CCScript").Element();
  hEntry = TiXmlHandle(entryNode);
  entryNode = hEntry.FirstChild("Script").Element();
  for (; entryNode; entryNode = entryNode->NextSiblingElement()) {
    string* a = new string();
    a->assign(entryNode->GetText());
    scriptFiles.push_back(a);
  }
}

void writeProjectFile(char* dir, map<string, string> &filesMap, char* romtype) {
  char buf[100];
  strcpy(buf, dir);
  strcat(buf, "/project.ebproj");

  TiXmlDocument doc;
  TiXmlDeclaration * decl = new TiXmlDeclaration( "1.0", "", "" );
  doc.LinkEndChild( decl );

  TiXmlElement* root = new TiXmlElement("Project");
  doc.LinkEndChild(root);

  TiXmlElement *file, *files;
  file = new TiXmlElement("RomType");
  root->LinkEndChild(file);
  file->LinkEndChild(new TiXmlText(romtype));
  files = new TiXmlElement("Files");
  root->LinkEndChild(files);
  map<string, string>::iterator it;
  for (it = filesMap.begin(); it != filesMap.end(); ++it) {
    file = new TiXmlElement("File");
    files->LinkEndChild(file);
    file->SetAttribute("id", it->first);
    file->LinkEndChild(new TiXmlText(it->second));
  }

  file = new TiXmlElement("CCScript");
  root->LinkEndChild(file);

  doc.SaveFile(buf);
}

// TODO make this better, this is lazy
// should be more ike JHack
void findRomType(Rom &rom, char* out) {
  if ((rom.read(0x1f005) == 0xa9) && (rom.read(0x1f006 == 0x5e)))
    strcpy(out, "EARTHBOUND");
  else
    strcpy(out, "UNKNOWN");
}

bool dir_exists(char* dir) {
#ifdef WIN32
    DWORD dwAttr = GetFileAttributes(dir);
    return (dwAttr != 0xffffffff && (dwAttr & FILE_ATTRIBUTE_DIRECTORY));
#else
    struct stat st;
    return (stat(dir,&st) == 0);
#endif
}

void readCccSummary(const char* fname, uint &firstFreePos, map<string, int> &labels) {
  ifstream ifs(fname);
  string line;
  int pos, pos2;
  bool labelSec = false;
  while (getline(ifs, line)) {
    if (labelSec && ((pos = line.find_first_of('$')) >= 0)) {
      pos2 = line.find_first_of(' ');
//      cout << "ADDING LABEL " << (line.substr(0,pos2)) << endl;
      labels.insert(pair<string, int>(line.substr(0,pos2), toRegPointer(atoi_b(line.substr(pos+1).c_str(), 16))));
    } else if (!line.compare(0, strlen("Compilation end:"), "Compilation end:")) {
      pos = line.find_first_of('$');
      firstFreePos = toRegPointer(atoi_b(line.substr(pos+1).c_str(), 16));
    } else if (!line.compare(0, strlen("Label locations"), "Label locations")) {
      labelSec = true;
    }
  }
} 

int main(int argc, char* argv[]) {
#ifndef WIN32
  struct stat st;
#endif

  if (argc < 2) {
    usage(argv[0]);
    return 0;
  }

  int i;
  char *cccExe = 0, *inRom = 0, *outRom = 0, *projDir = 0;
  bool importCmd = false, exportCmd = false;
  for (i=1; i<argc-1; i++) {
    if ((strcmp(argv[i], "i") == 0) || (strcmp(argv[i], "import") == 0)) {
      if (!exportCmd)
        importCmd = true;
    } else if ((strcmp(argv[i], "e") == 0) || (strcmp(argv[i], "export") == 0)) {
      if (!importCmd)
        exportCmd = true;
    } else if (strcmp(argv[i], "-i") == 0) {
      if (importCmd)
        inRom = argv[++i];
      else if (exportCmd)
        projDir = argv[++i];
    } else if (strcmp(argv[i], "-o") == 0) {
      if (importCmd)
        projDir = argv[++i];
      else if (exportCmd)
        outRom = argv[++i];
    } else if ((strcmp(argv[i], "-r") == 0) || (strcmp(argv[i], "--cleanRom") == 0)) {
      if (exportCmd)
        inRom = argv[++i];
    } else if ((strcmp(argv[i], "-c") == 0) || (strcmp(argv[i], "--ccscript") == 0)) {
      cccExe = argv[++i];
    }
  }

  GameModule *gm;
  Rom rom;
  char romtype[11];
  map<string, string> files;
  list<string*> scriptFiles;

  if (importCmd) {
    cout << "Importing..." << endl;

    if (!inRom || !projDir) {
      cerr << "Error: Not enough arguments." << endl;
      usage(argv[0]);
      return 0;
    }

    if(dir_exists(projDir))
    	readProjectFile(projDir, files, scriptFiles, romtype);
#ifdef WIN32
    else if (mkdir(projDir) == -1) {
#else
    else if (stat(projDir,&st) && (mkdir(projDir, 0777) == -1)) {
#endif
      cerr << "Error creating directory '" << argv[3] << "'" << endl;
      return -1;
    }

    rom.open(inRom);
    findRomType(rom, romtype);
    if (strcmp(romtype, "EARTHBOUND") == 0)
      gm = new EbModule();
    else
      gm = new GenericGameModule();
    gm->init();
    gm->readFromRom(rom);
    gm->writeToDat(projDir, files);
    writeProjectFile(projDir, files, romtype);
  } else if (exportCmd) {
    cout << "Exporting..." << endl;

    if (!projDir || !inRom || !outRom || !cccExe) {
      cerr << "Error: Not enough arguments." << endl;
      usage(argv[0]);
      return 0;
    }

    if (!dir_exists(projDir)) {
      cerr << "Error: Directory '" << projDir << "' doesn't exist." << endl;
      return -1;
    }

    readProjectFile(projDir, files, scriptFiles, romtype);
    if (strcmp(romtype, "EARTHBOUND") == 0)
      gm = new EbModule();
    else
      gm = new GenericGameModule();
    gm->init();
//    gm->readFromDat(projDir, files);

    map<string, int> labels;

    // Create and expand ROM
    rom.openExpand(inRom);

    if (scriptFiles.size() > 0) {
      cout << "Compiling scripts..." << endl;
      // Save expanded ROM
      rom.save(outRom);
      rom.close();

      // Call CCscript externally
      string cccCommand;
//      cccCommand.append("\"");
      cccCommand.append(cccExe);
      cccCommand.append(" -n -s F00000 --summary /tmp/ccc_temp.txt -o ");
      cccCommand.append(outRom);
      list<string*>::iterator it;
      for (it = scriptFiles.begin(); it != scriptFiles.end(); it++) {
        cccCommand.append(" ");
        cccCommand.append(projDir);
        cccCommand.append("/");
        cccCommand.append(*(*it));
      }
      cout << "* " << cccCommand << endl;
      system(cccCommand.c_str());

      uint firstFreePos;
      readCccSummary("/tmp/ccc_temp.txt", firstFreePos, labels);
      remove("/tmp/ccc_temp.txt");


      // Open the modified ROM
      rom.open(outRom);
      rom.setFirstFreePos(firstFreePos);
    }
    gm->readFromDat(projDir, files, labels);
    gm->writeToRom(rom);
    rom.save(outRom);
  } else {
    cerr << "Error: Invalid command specified" << endl;
    usage(argv[0]);
    return -1;
  }

  gm->destroy();
  delete gm;

  list<string*>::iterator it;
  for (it = scriptFiles.begin(); it != scriptFiles.end(); it++)
    delete (*it);


  // Delete filename data
  /*for (vector<char**>::iterator i = files.begin(); i != files.end(); ++i) {
    delete[] (*i)[0];
    delete[] (*i)[1];
    delete[] (*i);
  }*/

  return 0;
}
